package com.logicfishsoftware.wolfgo.mount.dokan;

import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;

import net.decasdev.dokan.ByHandleFileInformation;
import net.decasdev.dokan.CreationDisposition;
import net.decasdev.dokan.Dokan;
import net.decasdev.dokan.DokanDiskFreeSpace;
import net.decasdev.dokan.DokanFileInfo;
import net.decasdev.dokan.DokanOperationException;
import net.decasdev.dokan.DokanOperations;
import net.decasdev.dokan.DokanOptions;
import net.decasdev.dokan.DokanOptionsMode;
import net.decasdev.dokan.DokanVolumeInformation;
import net.decasdev.dokan.FileAccess;
import net.decasdev.dokan.FileAttribute;
import net.decasdev.dokan.FileFlag;
import net.decasdev.dokan.FileTimeUtils;
import net.decasdev.dokan.Win32FindData;
import net.decasdev.dokan.WinError;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.logicfishsoftware.wolfgo.api.VirtualContentConnection;
import com.logicfishsoftware.wolfgo.api.VirtualDirectory;
import com.logicfishsoftware.wolfgo.api.VirtualFile;
import com.logicfishsoftware.wolfgo.api.VirtualFileSystem;
import com.logicfishsoftware.wolfgo.api.VirtualFileSystemConnection;
import com.logicfishsoftware.wolfgo.api.VirtualFileSystemException;

public class VirtualMount_Dokan implements DokanOperations {
	static final Logger log = LoggerFactory.getLogger(VirtualMount_Dokan.class);
	
    public static final int FILE_CASE_PRESERVED_NAMES = 0x00000002;
    public static final int FILE_FILE_COMPRESSION = 0x00000010;
    public static final int FILE_SUPPORTS_SPARSE_FILES = 0x00000040;
    public static final int FILE_UNICODE_ON_DISK = 0x00000004;

    public static final int SUPPORTED_FLAGS = FILE_CASE_PRESERVED_NAMES
            | FILE_UNICODE_ON_DISK | FILE_SUPPORTS_SPARSE_FILES;
	/** fileName -> FileDevice */
//	final ConcurrentHashMap<String, FileDevice> fileInfoMap = new ConcurrentHashMap<String, FileDevice>();
	// TODO FIX THIS
	final static int volumeSerialNumber = 5757;
	/** Next handle */
	long nextHandleNo = 1;
	final long rootCreateTime = FileTimeUtils.toFileTime(new Date());
	long rootLastWrite = rootCreateTime;
	private String driveLetter;

	private VirtualFileSystem filesys;

	private VirtualMount_Dokan(VirtualFileSystem filesys) {
		this.filesys = filesys;
	}

	static void log(String msg) {
		log.info(msg);
	}
	
	void mount(String driveLetter,int threadCount) {
		log("[mount] "+driveLetter);
		DokanOptions dokanOptions = new DokanOptions();
        dokanOptions.mountPoint = driveLetter;
        dokanOptions.threadCount = threadCount;
        dokanOptions.optionsMode = DokanOptionsMode.Mode.REMOVABLE_DRIVE.getValue() + DokanOptionsMode.Mode.KEEP_ALIVE.getValue();
        this.driveLetter = driveLetter;
		int result = Dokan.mount(dokanOptions, this);
		log("[VirtualMount_Dokan] result = " + result);

        if (result < 0) {
            System.out.println("Unable to mount volume because result = "
                    + result);
            log("Unable to mount volume because result = " + result);
            if (result == -1)
                System.out.println("General Error");
            if (result == -2)
                System.out.println("Bad Drive letter");
            if (result == -3)
                System.out.println("Can't install driver");
            if (result == -4)
                System.out.println("Driver something wrong");
            if (result == -5)
                System.out
                        .println("Can't assign a drive letter or mount point");
            if (result == -6)
                System.out.println("Mount point is invalid");
            System.exit(-1);

        } else {
            log("######## mounted " + driveLetter + " with result " + result
                    + " #############");
        }
	}

	synchronized long getNextHandle() {
		return nextHandleNo++;
	}

	public long onCreateFile(String fileName, int desiredAccess, int shareMode, int creationDisposition,
			int flagsAndAttributes, DokanFileInfo arg5) throws DokanOperationException {
        CreationDisposition disposition = CreationDisposition.build(creationDisposition);

		log("[onCreateFile] " + fileName +
                ",\n\t creationDisposition = " + creationDisposition
                + ",\n\t desiredAccess = " + FileAccess.toString(desiredAccess)
                + ",\n\t flags = " + FileFlag.toString(flagsAndAttributes)
                + ",\n\t attributes = " + FileAttribute.toString(flagsAndAttributes));

		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			return _createFile(connection,fileName,desiredAccess,shareMode,disposition,flagsAndAttributes,arg5);
		} catch (Exception e) {
			e.printStackTrace(System.err);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
	}

	private long _createFile(VirtualFileSystemConnection connection, String fileName,
			int desiredAccess, int shareMode, CreationDisposition disposition,
			int flagsAndAttributes, DokanFileInfo arg5) throws DokanOperationException, VirtualFileSystemException {
		if (fileName.equals("\\")) {
			switch (disposition) {
			case CREATE_NEW:
			case CREATE_ALWAYS:
				throw new DokanOperationException(WinError.ERROR_ALREADY_EXISTS);
			case OPEN_ALWAYS:
			case OPEN_EXISTING:
			case TRUNCATE_EXISTING:
				return getNextHandle();
            case UNDEFINED:
                assert(false);
			}
		} else if (connection.exists(fileName)) {
			switch (disposition) {
			case CREATE_NEW:
				throw new DokanOperationException(WinError.ERROR_ALREADY_EXISTS);
			case OPEN_ALWAYS:
			case OPEN_EXISTING:
				return getNextHandle();
			case CREATE_ALWAYS:
			case TRUNCATE_EXISTING:
				//fileInfoMap.get(fileName).content.clear();
				connection.getContentConnection().clear(fileName);
				updateParentLastWrite(connection,fileName);
				return getNextHandle();
            case UNDEFINED:
                assert(false);
			}
		} else {
			switch (disposition) {
			case CREATE_NEW:
			case CREATE_ALWAYS:
			case OPEN_ALWAYS:
				FileDevice fi = new FileDevice(fileName, false);
				//fileInfoMap.put(fi.fileName, fi);
				//connection.put(fi.getFileName(),fi);
				putFile(connection,fi);
				updateParentLastWrite(connection,fileName);
				return getNextHandle();
			case OPEN_EXISTING:
			case TRUNCATE_EXISTING:
				throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
            case UNDEFINED:
                assert(false);
			}
		}
		throw new DokanOperationException(WinError.ERROR_INVALID_FUNCTION);
	}

	private void putFile(VirtualFileSystemConnection connection, FileDevice fi) throws VirtualFileSystemException {
		VirtualFile file;
		if(fi.isDirectory()) {
			file = connection.put(fi.getFileName(), VirtualDirectory.class);
		} else {
			file = connection.put(fi.getFileName(), VirtualFile.class);			
		}
		// TODO fill in file details
	}

	public long onOpenDirectory(String pathName, DokanFileInfo arg1) throws DokanOperationException {
		log("[onOpenDirectory] " + pathName);
		if (pathName.equals("\\"))
			return getNextHandle();

		pathName = Utils.trimTailBackSlash(pathName);
        log("[onOpenDirectory] step 2");
        VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			if (connection.dirExists(pathName))
				return getNextHandle();
			else
				throw new DokanOperationException(WinError.ERROR_PATH_NOT_FOUND);
		} catch (VirtualFileSystemException e) {
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
	}

	public void onCreateDirectory(String fileName, DokanFileInfo file) throws DokanOperationException {
		log("[onCreateDirectory] " + fileName);
		fileName = Utils.trimTailBackSlash(fileName);
        VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			if (connection.exists(fileName) || fileName.length() == 0)
				throw new DokanOperationException(WinError.ERROR_ALREADY_EXISTS);
			FileDevice fi = new FileDevice(fileName, true);
			//fileInfoMap.put(fi.fileName, fi);
//			connection.put(fileName, fi);
			putFile(connection,fi);
			updateParentLastWrite(connection,fileName);
	        log("[onCreateDirectory] END");
		} catch (Exception e) {
			log.error("Error creating directory.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();			
		}
	}

	public void onCleanup(String fileName, DokanFileInfo fileInfo) throws DokanOperationException {
        log("[onCleanup] " + fileName);
	}

	public void onCloseFile(String fileName, DokanFileInfo fileInfo) throws DokanOperationException {
        log("[onCloseFile] " + fileName);
	}

	public int onReadFile(String fileName, ByteBuffer buffer, long offset, DokanFileInfo arg3)
			throws DokanOperationException {
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			return _readFile(connection,fileName,buffer,offset,arg3);
		} catch (VirtualFileSystemException e) {
			throw new DokanOperationException(WinError.ERROR_READ_FAULT);
		} finally {
			if(connection!=null)connection.close();
		}
	}
	public int _readFile(VirtualFileSystemConnection connection, String fileName, ByteBuffer buffer, long offset, DokanFileInfo arg3) throws DokanOperationException {
		log("[onReadFile] " + fileName);
		FileDevice fi;
		try {
			fi = new FileDevice(connection.get(fileName));
			fi.setFileSize((int) connection.getContentConnection().length(fileName));
		} catch (VirtualFileSystemException e1) {
			throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
		}
//		if (fi == null)
//			throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
		if (fi.getFileSize() == 0)
			return 0;
		try {
			int copySize = Math.min(buffer.capacity(), fi.getFileSize() - (int) offset);
			if (copySize <= 0)
				return 0;
			connection.getContentConnection().read(fileName,buffer,offset,copySize);
			//buffer.put(fi.content.toArray((int) offset, copySize));
			return copySize;
		} catch (Exception e) {
			e.printStackTrace();
			throw new DokanOperationException(WinError.ERROR_READ_FAULT);
		}
	}

	public int onWriteFile(String fileName, ByteBuffer buffer, long offset, DokanFileInfo arg3)
			throws DokanOperationException {
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			return _writeFile(connection,fileName,buffer,offset,arg3);
		} catch (VirtualFileSystemException e) {
			throw new DokanOperationException(WinError.ERROR_WRITE_FAULT);
		} finally {
			if(connection!=null)connection.close();
		}
	}
	public int _writeFile(VirtualFileSystemConnection connection,String fileName, ByteBuffer buffer, long offset, DokanFileInfo arg3)
			throws DokanOperationException {
		log("[onWriteFile] " + fileName);
		FileDevice fi;
		try {
			fi = new FileDevice(connection.get(fileName));
		} catch (VirtualFileSystemException e1) {
			throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
		}
//		if (fi == null) {
//			log("[onWriteFile] file not found");
//			throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
//		}
		try {
			int copySize = buffer.capacity();
			byte[] tmpBuff = new byte[copySize];
			buffer.get(tmpBuff);
			VirtualContentConnection content = connection.getContentConnection();
			int overwriteSize = Math.min((int) offset + copySize, fi.getFileSize()) - (int) offset;
			if (overwriteSize > 0)
				//fi.content.set((int) offset, tmpBuff, 0, overwriteSize);
				content.write(fileName,offset,tmpBuff,0,overwriteSize);
			int addSize = copySize - overwriteSize;
			if (addSize > 0)
				content.append(fileName, tmpBuff, overwriteSize, addSize);
			fi.setLastWriteTime(FileTimeUtils.toFileTime(new Date()));
			fi.setFileSize((int)content.length(fileName));
			return copySize;
		} catch (Exception e) {
			log.error("Write error.",e);
			throw new DokanOperationException(WinError.ERROR_WRITE_FAULT);
		}
	}

	public void onSetEndOfFile(String fileName, long length, DokanFileInfo arg2)
			throws DokanOperationException {
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			_setEndOfFile(connection, fileName, length, arg2);
		} catch (VirtualFileSystemException e) {
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
		
	}
	public void _setEndOfFile(VirtualFileSystemConnection connection,String fileName, long length, DokanFileInfo arg2)
				throws DokanOperationException {
		log("[onSetEndOfFile] " + fileName);
		FileDevice fi;
		try {
			fi = new FileDevice(connection.get(fileName));
		} catch (VirtualFileSystemException e) {
			throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
		}
//		if (fi == null)
//			throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
		if (fi.getFileSize() == length)
			return;
		if (fi.getFileSize() < length) {
			byte[] tmp = new byte[(int) length - fi.getFileSize()];
			connection.getContentConnection().append(fileName,tmp);
		} else {
			//fi.content.remove((int) length, fi.getFileSize() - (int) length);
			connection.getContentConnection().truncate(fileName,length);
		}
	}

	public void onFlushFileBuffers(String fileName, DokanFileInfo fileInfo) throws DokanOperationException {
        log("[onFlushFileBuffers] " + fileName);
	}

	public ByHandleFileInformation onGetFileInformation(String fileName, DokanFileInfo arg1)
			throws DokanOperationException {
		log("[onGetFileInformation] " + fileName);
		if (fileName.equals("\\")) {
			return new ByHandleFileInformation(
                    FileAttribute.FileAttributeFlags.FILE_ATTRIBUTE_NORMAL.getValue() |
                    FileAttribute.FileAttributeFlags.FILE_ATTRIBUTE_DIRECTORY.getValue(),
					rootCreateTime, rootCreateTime, rootLastWrite, VirtualMount_Dokan.volumeSerialNumber, 0, 1, 1);
		}
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			FileDevice fi = new FileDevice(connection.get(fileName));
			fi.setFileSize((int)connection.getContentConnection().length(fileName));
//			if (fi == null)
//				throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
			return fi.toByHandleFileInformation();
		} catch (VirtualFileSystemException e) {
			log.error("Error reading file info.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
	}

	public Win32FindData[] onFindFiles(String pathName, DokanFileInfo arg1)
			throws DokanOperationException {
		log("[onFindFiles] " + pathName);
		VirtualFileSystemConnection connection = null;
		Collection<VirtualFile> fis = null;
		try {
			connection = filesys.connect();
			fis = connection.getChildren(pathName);
		} catch (VirtualFileSystemException e) {
			log.error("Error finding files.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
		ArrayList<Win32FindData> files = new ArrayList<Win32FindData>();
//		File pathNameFile = new File(pathName);
		for (VirtualFile fi : fis) {
//			if (pathNameFile.equals(new File(fi.fileName).getParentFile())) {
//				files.add(fi.toWin32FindData());
//			}
			files.add(new FileDevice(fi).toWin32FindData());
        }
		return files.toArray(new Win32FindData[0]);
	}

	public Win32FindData[] onFindFilesWithPattern(String arg0, String arg1, DokanFileInfo arg2)
			throws DokanOperationException {
        log("[onFindFilesWithPattern] ");
		return null;
	}

	public void onSetFileAttributes(String fileName, int fileAttributes, DokanFileInfo arg2)
			throws DokanOperationException {
		log("[onSetFileAttributes] " + fileName);
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
//			FileDevice fi = new FileDevice(connection.get(fileName));
//			if (fi == null)
//				throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
//			fi.setFileAttribute(fileAttributes);
			connection.setAttributes(fileName, fileAttributes);
		} catch (VirtualFileSystemException e) {
			log.error("Error setting file attributes.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
		//meta.setAttr()
	}

	public void onSetFileTime(String fileName, long creationTime, long lastAccessTime,
			long lastWriteTime, DokanFileInfo arg4) throws DokanOperationException {
		log("[onSetFileTime] " + fileName);
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			VirtualFile fi = connection.get(fileName);
//			FileDevice fi = new FileDevice(connection.get(fileName));
//			if (fi == null)
//				throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
			fi.setCreationTime(creationTime);
			fi.setLastAccessTime(lastAccessTime);
			fi.setLastWriteTime(lastWriteTime);
		} catch (VirtualFileSystemException e) {
			log.error("Error setting file time.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
	}

	public void onDeleteFile(String fileName, DokanFileInfo arg1) throws DokanOperationException {
		log("[onDeleteFile] " + fileName);
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			//FileDevice fi = connection.get(fileName);
			FileDevice removed = new FileDevice(connection.delete(fileName));
//			if (removed == null)
//				throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
			connection.getContentConnection().delete(fileName);
			updateParentLastWrite(connection,fileName);
		} catch (VirtualFileSystemException e) {
			log.error("Error deleting file.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
	}

	public void onDeleteDirectory(String fileName, DokanFileInfo arg1) throws DokanOperationException {
		log("[onDeleteDirectory] " + fileName);
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();
			FileDevice removed = new FileDevice(connection.delete(fileName));
//			if (removed == null)
//				throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
			updateParentLastWrite(connection,fileName);
		} catch (VirtualFileSystemException e) {
			log.error("Error deleting directory.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
	}

	public void onMoveFile(String existingFileName, String newFileName, boolean replaceExisiting,
			DokanFileInfo arg3) throws DokanOperationException {
		log("==> [onMoveFile] " + existingFileName + " -> " + newFileName + ", replaceExisiting = "
				+ replaceExisiting);
		VirtualFileSystemConnection connection = null;
		try {
			connection = filesys.connect();		
			FileDevice existing = new FileDevice(connection.get(existingFileName));
//			if (existing == null)
//				throw new DokanOperationException(WinError.ERROR_FILE_NOT_FOUND);
			// TODO Fix this
			if (existing.isDirectory())
				throw new DokanOperationException(WinError.ERROR_DIRECTORY);
			FileDevice newFile = new FileDevice(connection.get(newFileName));
			if (newFile != null && !replaceExisiting)
				throw new DokanOperationException(WinError.ERROR_FILE_EXISTS);
			connection.delete(existingFileName);
			existing.setFileName(newFileName);
//			connection.put(newFileName, existing);
			putFile(connection,existing);
			connection.getContentConnection().move(existingFileName,newFileName);
			updateParentLastWrite(connection,existingFileName);
			updateParentLastWrite(connection,newFileName);
			log("<== [onMoveFile]");
		} catch (VirtualFileSystemException e) {
			log.error("Error moving file.",e);
			throw new DokanOperationException(WinError.ERROR_OPEN_FAILED);
		} finally {
			if(connection!=null)connection.close();
		}
	}

	public void onLockFile(String fileName, long arg1, long arg2, DokanFileInfo arg3)
			throws DokanOperationException {
		log("[onLockFile] " + fileName);
	}

	public void onUnlockFile(String fileName, long arg1, long arg2, DokanFileInfo arg3)
			throws DokanOperationException {
		log("[onUnlockFile] " + fileName);
	}


	public DokanDiskFreeSpace onGetDiskFreeSpace(DokanFileInfo fileInfo) throws DokanOperationException {
        log("[onGetDiskFreeSpace] ");
        DokanDiskFreeSpace free = new DokanDiskFreeSpace();
        //TODO: this is completely wrong, the size is not calculated
        free.totalNumberOfBytes = 1024L * 1024L;
        free.freeBytesAvailable = free.totalNumberOfFreeBytes / 2;
        free.totalNumberOfFreeBytes = free.freeBytesAvailable;
		return free;
	}

	public DokanVolumeInformation onGetVolumeInformation(String arg0, DokanFileInfo arg1)
			throws DokanOperationException {
        DokanVolumeInformation info = new DokanVolumeInformation();
        info.fileSystemFlags = SUPPORTED_FLAGS;
        info.maximumComponentLength = 256;
        info.volumeName = "WolfGo Filesystem";
        info.fileSystemName = "SDFS";
        info.volumeSerialNumber = VirtualMount_Dokan.volumeSerialNumber;
        return info;
	}

	public void onUnmount(DokanFileInfo arg0) throws DokanOperationException {
		log("[onUnmount]");
        Dokan.removeMountPoint(driveLetter);
	}

	void updateParentLastWrite(VirtualFileSystemConnection connection,String fileName) {
//        log("[updateParentLastWrite]");
//		if (fileName.length() <= 1)
//			return;
//		String parent = new File(fileName).getParent();
//		log("[updateParentLastWrite] parent = " + parent);
////		if (parent == "\\") {
////			rootLastWrite = FileTimeUtils.toFileTime(new Date());
////		} else {
//			FileDevice fi;
//			try {
////				fi = new FileDevice(connection.get(parent));
//				fi = new FileDevice(connection.get(fileName).getParent());
//			} catch (VirtualFileSystemException e) {
//				return;
//			}
////			if (fi == null)
////				return;
//			fi.setLastWriteTime(FileTimeUtils.toFileTime(new Date()));
////		}
	}

	public static void start(final String driveLetter, final VirtualFileSystem filesys, int threadCount) {
		new VirtualMount_Dokan(filesys).mount(driveLetter,threadCount);
	}
	public static void startThread(final String driveLetter, final VirtualFileSystem fileSystem,final int threadCount) {
		Thread thread = new Thread(new Runnable(){
			@Override
			public void run() {
				try { 
					start(driveLetter,fileSystem,threadCount);
				} finally {
					log("Closing...");
					fileSystem.shutdown();					
				}				
			}});
		thread.start();
	}
}
